package mirrormonkey.rpc.member;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;

import mirrormonkey.framework.SyncAppState;
import mirrormonkey.framework.entity.EntityData;
import mirrormonkey.framework.entity.StaticEntityData;
import mirrormonkey.framework.entity.StaticEntityData.MemberDataKey;
import mirrormonkey.framework.member.DynamicMemberData;
import mirrormonkey.framework.member.StaticMemberData;
import mirrormonkey.framework.parameter.ParameterInterpreter;
import mirrormonkey.framework.parameter.ParameterInterpreterUtil;
import mirrormonkey.rpc.annotations.RpcTarget;
import mirrormonkey.rpc.messages.RpcCallMessage;
import mirrormonkey.rpc.messages.RpcErrorMessage;
import mirrormonkey.rpc.messages.RpcResultMessage;
import mirrormonkey.rpc.module.CallRegistry;

import com.jme3.network.Message;
import com.jme3.network.MessageConnection;

public class RpcMethodData implements StaticMemberData {

	public static final String DISALLOWED_INBOUND_ERROR = "Inbound calls are not allowed for this method.";

	public static final String REMOTE_TIMEOUT_ERROR = "Call not invoked because it timed out.";

	public final int id;

	public final Method method;

	public final ParameterInterpreter[] paramAccessors;

	public final Object[] paramCache;

	public final ParameterInterpreter resultAccessor;

	public final MemberDataKey memberKey;

	public final boolean localInvokeBefore;

	public final boolean localInvokeAfter;

	public final boolean reliable;

	public final boolean allowInbound;

	public final float timeout;

	public final float remoteTimeout;

	public RpcMethodData(int id, Method method,
			ParameterInterpreter[] paramAccessors,
			ParameterInterpreter resultAccessor, MemberDataKey memberKey,
			boolean localInvokeBefore, boolean localInvokeAfter,
			boolean reliable, boolean allowInbound, float timeout,
			float remoteTimeout) {
		this.id = id;
		this.method = method;
		this.paramAccessors = paramAccessors;
		this.paramCache = new Object[paramAccessors.length];
		this.resultAccessor = resultAccessor;
		this.memberKey = memberKey;
		this.localInvokeBefore = localInvokeBefore;
		this.localInvokeAfter = localInvokeAfter;
		this.reliable = reliable;
		this.allowInbound = allowInbound;
		this.timeout = timeout;
		this.remoteTimeout = remoteTimeout;
	}

	public Message executeFromRemote(MessageConnection source,
			EntityData entityData, RpcCallMessage message, SyncAppState appState) {
		if (!allowInbound) {
			return message.callId != CallRegistry.NO_RESPONSE_EXPECTED ? new RpcErrorMessage(
					message.callId, new RuntimeException(
							DISALLOWED_INBOUND_ERROR), message.reliable) : null;
		}
		if (remoteTimeout != RpcTarget.NO_CALL_TIMEOUT
				&& ((message.estdInvocationTime + remoteTimeout) < appState.timer
						.getTime())) {
			return message.callId != CallRegistry.NO_RESPONSE_EXPECTED ? new RpcErrorMessage(
					message.callId, new RuntimeException(REMOTE_TIMEOUT_ERROR),
					message.reliable) : null;
		}
		try {
			int i = 0;
			for (ParameterInterpreter a : paramAccessors) {
				paramCache[i] = a.extractData(message.parameters[i++]);
			}
			Object result;
			result = method.invoke(entityData.entity, paramCache);
			result = resultAccessor.packData(result);
			return message.callId != CallRegistry.NO_RESPONSE_EXPECTED ? new RpcResultMessage(
					message.callId, result, message.reliable) : null;
		} catch (InvocationTargetException e) {
			e.printStackTrace();
			return new RpcErrorMessage(message.callId, e.getCause(),
					message.reliable);
		} catch (Throwable e) {
			e.printStackTrace();
			return new RpcErrorMessage(message.callId, e, message.reliable);
		}
	}

	public Object extractResult(Object result) {
		return resultAccessor.extractData(result);
	}

	@Override
	public MemberDataKey getMemberKey() {
		return memberKey;
	}

	public static final class RpcMethodKey implements MemberDataKey {

		public final String name;

		public final Class<?>[] argTypes;

		public RpcMethodKey(String name, Class<?>[] argTypes) {
			this.name = name;
			this.argTypes = argTypes;
		}

		@Override
		public int hashCode() {
			return 13 * name.hashCode() + argTypes.length;
		}

		@Override
		public boolean equals(Object o) {
			if (!o.getClass().equals(RpcMethodKey.class)) {
				return false;
			}
			RpcMethodKey other = (RpcMethodKey) o;
			return name.equals(other.name)
					&& Arrays.deepEquals(argTypes, other.argTypes);
		}

	}

	public Object[] packParams(Object[] args) {
		return ParameterInterpreterUtil.packParams(args, paramAccessors);
	}

	@Override
	public DynamicMemberData createDynamicData() {
		return null;
	}

	@Override
	public void setStaticEntityData(StaticEntityData staticData) {
	}

}
